package com.nulldev.util.networking.npacket4;

import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__readASCII;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__readInt;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__writeASCII;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__writeInt;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.data.Variables;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.networking.npacket4.extensions.PacketExtension;
import com.nulldev.util.networking.npacket4.extensions.PacketExtensions;
import com.nulldev.util.networking.npacket4.extensions.protocol.PacketCompressionExtension;
import com.nulldev.util.networking.npacket4.extensions.security.PacketSecurer;
import com.nulldev.util.networking.npacket4.extensions.security.SecureCommunicationExtension;

/**
 * nPacket v4, a massive improvement to v3. <br>
 * Changes: <br>
 * <ul>
 * <li>Standardised Packet Layout</li>
 * <li>Integrated compression and encryption.</li>
 * <li>Uses extension system over packets and compat. [and API levels]</li>
 * <li>Heavy focus on byte-based packets over strings</li>
 * </ul>
 * <br>
 * Packet Layout: <br>
 * <ul>
 * <li>PACKET_ID (String)</li>
 * <li>EXTENSION_COUNT</li>
 * <li>EXTENSION_ID (If COUNT > 0)</li>
 * <li>DATA_SIZE</li>
 * <li>DATA (byte[])</li>
 * <li>EXTENSIONS_COUNT</li>
 * <li>EXTENSIONS (if EXTENSIONS_COUNT > 0)</li>
 * </ul>
 * 
 * @author null
 * @since 3.0
 */
public abstract class nPacket implements Serializable {

	/**
	 * TODO for 4.0.1: <br />
	 * - Add MUX support (multi-stream packets, chunked MUX packets) <br />
	 * - Add support for multi-stream packet references. <br />
	 * - Add support for reading from multiple streams. <br />
	 * - Move away from volatile and use atomics. (DONE) <br />
	 * TODO for 4.0.2: <br />
	 * - Add CNL support. (packet/message channels) <br />
	 * TODO for 4.0.3: <br />
	 * - Add MPR support. (multipath routing) <br />
	 * Requires: PPT (Packet Passthrough)
	 */

	private static final long serialVersionUID = -7003067909253980791L;

	public static final int NPACKET_HEADER = 77490; // NP4 -> hashCode()
	public static final String NPACKET_VERSION = "4.0.0";

	private PacketExtension[] packetExts = new PacketExtension[0];
	private int[] packetExtIDs = new int[0];
	private String packetID;
	private byte[] data;
	private byte[] raw_data;
	private PacketSecurer sec;

	public nPacket() {
	}

	public nPacket(final nPacket packet) {
		if (packet == null)
			throw new NullPointerException("packet is null!");
		this.packetExtIDs = packet.packetExtIDs;
		this.packetExts = packet.packetExts;
		this.packetID = packet.packetID;
		this.data = packet.data;
		this.raw_data = packet.raw_data;
		this.sec = packet.sec;
		try {
			final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(this.data));
			this.onPacket(dis);
			IOUtils.closeQuietly(dis);
		} catch (IOException e) {
			JVM.errorln(e);
		}
	}

	public nPacket(final String packetID, final nPacket packet) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		if (packet == null)
			throw new NullPointerException("packet is null!");
		this.packetExtIDs = packet.packetExtIDs;
		this.packetExts = packet.packetExts;
		this.packetID = packetID;
		this.data = packet.data;
		this.raw_data = packet.raw_data;
		this.sec = packet.sec;
		try {
			final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(this.data));
			this.onPacket(dis);
			IOUtils.closeQuietly(dis);
		} catch (IOException e) {
			JVM.errorln(e);
		}
	}

	public nPacket(final String packetID, final PacketExtension... exts) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		this.packetID = packetID;
		this.packetExts = exts;
		this.packetExtIDs = new int[exts.length];
		for (int i = 0; i < exts.length; i++) {
			this.packetExtIDs[i] = exts[i].extensionID();
		}
	}

	public nPacket(final String packetID, final PacketSecurer sec, final PacketExtension... exts) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		if (sec == null)
			throw new NullPointerException("sec is null!");
		this.packetID = packetID;
		this.packetExts = exts;
		this.packetExtIDs = new int[exts.length];
		for (int i = 0; i < exts.length; i++) {
			this.packetExtIDs[i] = exts[i].extensionID();
		}
	}

	public nPacket(final PacketSecurer sec) throws IOException {
		if (sec == null)
			throw new NullPointerException("sec is null!");
		this.sec = sec;
	}

	public String getPacketID() {
		return this.packetID;
	}

	public nPacket readPacket(final byte[] data) throws IOException {
		return readPacket(data, null);
	}

	public nPacket readPacket(final byte[] data, final PacketSecurer sec) throws IOException {
		Variables.notNullE(data);
		if (data.length == 0)
			throw new IllegalArgumentException("data.length == 0!");
		this.raw_data = data;
		final ByteArrayInputStream bais = new ByteArrayInputStream(data);
		final DataInputStream dis = new DataInputStream(bais);
		final int header = __readInt(bais);
		if (header != NPACKET_HEADER) {
			throw new UnsupportedOperationException("nPacket4: Invalid packet! [header=" + header + "]");
		}
		this.packetID = __readASCII(bais);
		final int extensions = __readInt(bais);
		if (extensions < 0) {
			throw new UnsupportedOperationException("nPacket4: Invalid extensions count! [extensions=" + extensions + "]");
		}
		this.packetExts = new PacketExtension[extensions];
		this.packetExtIDs = new int[extensions];
		if (extensions > 0) {
			for (int i = 0; i < extensions; i++) {
				this.packetExtIDs[i] = __readInt(bais);
			}
		}
		final int size = __readInt(bais);
		if (size < 0) {
			throw new UnsupportedOperationException("nPacket4: Invalid size count! [size=" + size + "]");
		}
		byte[] _data = new byte[size];
		dis.read(_data);
		this.data = _data;
		final int extensionCount = dis.readInt();
		if (extensionCount > 0) {
			for (int i = 0; i < extensionCount; i++) {
				final int extID = dis.readInt();
				if (extID == PacketExtensions.PacketExtensionID.EXT_ENCRYPTED && sec != null) {
					this.packetExts[i] = new SecureCommunicationExtension(sec).readExtensionData(dis);
					this.data = this.packetExts[i].processData(this.data);
				} else if (extID == PacketExtensions.PacketExtensionID.EXT_COMPRESSION) {
					this.packetExts[i] = new PacketCompressionExtension().readExtensionData(dis);
					this.data = this.packetExts[i].processData(this.data);
				}
			}
		}
		dis.close();
		try (final DataInputStream _dis = new DataInputStream(new ByteArrayInputStream(this.data))) {
			this.onPacket(_dis);
		} catch (Exception e) {
			JVM.errorln(e);
		}
		return this;
	}

	public abstract void onPacket(final DataInputStream dis) throws IOException;

	public abstract void onGenerate(final DataOutputStream dos) throws IOException;

	public byte[] makePacket() {
		try {
			final ByteArrayOutputStream baos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
			final DataOutputStream dos = new DataOutputStream(baos);
			__writeInt(baos, NPACKET_HEADER);
			__writeASCII(baos, this.packetID);
			__writeInt(baos, this.packetExts.length);
			for (final int ext : this.packetExtIDs) {
				__writeInt(baos, ext);
			}
			final ByteArrayOutputStream dbaos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
			final DataOutputStream ddos = new DataOutputStream(dbaos);
			this.onGenerate(ddos);
			ddos.flush();
			this.data = dbaos.toByteArray();
			ddos.close();
			for (final PacketExtension ext : this.packetExts) {
				if (ext instanceof SecureCommunicationExtension) {
					if (this.sec != null) {
						this.data = sec.encrypt(this.data);
					} else {
						this.data = ((SecureCommunicationExtension) ext).securer().encrypt(this.data);
					}
				} else if (ext instanceof PacketCompressionExtension) {
					this.data = ((PacketCompressionExtension) ext).compress(this.data);
				}
			}
			__writeInt(baos, this.data.length);
			baos.flush();
			dos.write(this.data);
			dos.flush();
			dos.writeInt(this.packetExts.length);
			dos.flush();
			for (int i = 0; i < this.packetExts.length; i++) {
				final PacketExtension e = this.packetExts[i];
				final byte[] ed = e.extensionData();
				dos.writeInt(e.extensionID());
				dos.flush();
				dos.write(ed);
				dos.flush();
			}
			return baos.toByteArray();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public String toString() {
		return "nPacket[version=4,id=" + this.packetID + ",size=" + this.data.length + ",exts="
				+ Arrays.toString(PacketExtensions.getExtensionsReadable(this.packetExtIDs)) + "]";
	}

	public int[] getExtensionIDs() {
		return this.packetExtIDs;
	}

	public PacketExtension[] getExtensions() {
		return this.packetExts;
	}

	/* */

	public static nPacket createPacketFromBytes(final byte[] o) throws IOException {
		Variables.requireNonNull(o, "o");
		return new nPacket() {

			private static final long serialVersionUID = 1L;

			@Override
			public void onPacket(DataInputStream dis) throws IOException {
				/* no-op */
			}

			@Override
			public void onGenerate(DataOutputStream dos) throws IOException {
				/* no-op */
			}

		}.readPacket(o);
	}

	public static nPacket createPacketFromBytes(final byte[] o, final PacketSecurer sec) throws IOException {
		Variables.requireNonNull(o, "o");
		return new nPacket() {

			private static final long serialVersionUID = 1L;

			@Override
			public void onPacket(DataInputStream dis) throws IOException {
				/* no-op */
			}

			@Override
			public void onGenerate(DataOutputStream dos) throws IOException {
				/* no-op */
			}

		}.readPacket(o, sec);
	}
}
